package com.wyz.saas.common.view

import android.content.Context
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.Paint
import android.graphics.Rect
import android.os.Handler
import android.util.AttributeSet
import android.view.View
import android.view.ViewGroup
import com.wyz.saas.common.utils.DimensionConvert
import java.util.*
import kotlin.math.sin

class PitchView : ViewGroup {

    private inner class PitchCurrentView : View {
        var paint: Paint? = null
        var textPaint: Paint? = null
        var mText: String
        var textBounds: Rect? = null
        var dB = 0.0

        constructor(context: Context?) : this(context, null)

        constructor(
            context: Context?,
            attrs: AttributeSet?
        ) : this(context, attrs, 0)

        constructor(
            context: Context?,
            attrs: AttributeSet?,
            defStyleAttr: Int
        ) : super(context, attrs, defStyleAttr) {
            mText = "100 db"
            textBounds = Rect()
            textPaint = Paint()
            textPaint?.color = Color.GRAY
            textPaint?.isAntiAlias = true
            textPaint?.textSize = 20f
            paint = Paint()
            paint?.color = recColor
            paint?.strokeWidth = pitchWidth.toFloat()
        }

        override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
            val w = MeasureSpec.getSize(widthMeasureSpec)
            textPaint!!.getTextBounds(mText, 0, mText.length, textBounds)
            var h = paddingTop
            h += textBounds!!.height()
            h += DimensionConvert.dip2px(context, 2f)
            h += pitchWidth + paddingBottom
            setMeasuredDimension(w, h)
        }

        fun setText(text: String) {
            this.mText = text
            textPaint!!.getTextBounds(this.mText, 0, this.mText.length, textBounds)
        }

        fun update(end: Int) {
            dB = getDB(end) / 90
            var str = ""
            str =
                (getDB(end).toInt()).toString() + " db"
            setText(str)
        }

        override fun onDraw(canvas: Canvas) {
            if (data.size > 0) {
                current?.update(getEnd())
            }
            var y = paddingTop + textBounds!!.height().toFloat()
            val x = width / 2 - textBounds!!.width() / 2
            canvas.drawText(mText, x.toFloat(), y, textPaint!!)
            y += DimensionConvert.dip2px(context, 2f)
            val left = dB.toFloat()
            val right = dB.toFloat()
            val mid = width / 2f
            y += pitchWidth / 2.toFloat()
            canvas.drawLine(mid, y, mid - mid * left - 1, y, paint!!)
            canvas.drawLine(mid, y, mid + mid * right + 1, y, paint!!)
        }
    }

    private inner class PitchGraphView : View {
        private var paint: Paint = Paint()
        private var paintRed: Paint = Paint()
        private var editPaint: Paint = Paint()
        private var playPaint: Paint = Paint()
        private var cutColor: Paint = Paint()

        constructor(context: Context?) : this(context, null)

        constructor(
            context: Context?,
            attrs: AttributeSet?
        ) : this(context, attrs, 0)

        constructor(
            context: Context?,
            attrs: AttributeSet?,
            defStyleAttr: Int
        ) : super(context, attrs, defStyleAttr) {
            paint.color = this@PitchView.recColor
            paint.strokeWidth = pitchWidth.toFloat()
            paintRed.color = Color.RED
            paintRed.strokeWidth = pitchWidth.toFloat()
            cutColor.color = this@PitchView.cutColor
            cutColor.strokeWidth = pitchWidth.toFloat()
            editPaint.color = this@PitchView.recColor
            editPaint.strokeWidth = pitchWidth.toFloat()
            playPaint.color = this@PitchView.recColor
            playPaint.strokeWidth = pitchWidth / 2.toFloat()
        }

        override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
            super.onMeasure(widthMeasureSpec, heightMeasureSpec)
            val w = MeasureSpec.getSize(widthMeasureSpec)
            pitchScreenCount = w / pitchSize + 1
            pitchMemCount = pitchScreenCount + 1
        }

        override fun onLayout(
            changed: Boolean,
            left: Int,
            top: Int,
            right: Int,
            bottom: Int
        ) {
            super.onLayout(changed, left, top, right, bottom)
            fit(pitchScreenCount)
        }

        fun calc() {
            if (data.size >= pitchMemCount) {
                val cur = System.currentTimeMillis()
                var tick: Float = (cur - time) / mPitchTime.toFloat()

                // force clear queue
                if (data.size > pitchMemCount + 1) {
                    tick = 0f
                    time = cur
                    fit(pitchMemCount)
                }
                if (tick > 1) {
                    if (data.size > pitchMemCount) {
                        tick -= 1f
                        time += mPitchTime.toLong()
                    } else if (data.size == pitchMemCount) {
                        tick = 0f
                        time = cur
                    }
                    fit(data.size - 1)
                }
                offset = pitchSize * tick
            }
        }

        override fun onDraw(canvas: Canvas) {
            val m: Int = pitchMemCount.coerceAtMost(data.size)

            for (i in 0 until m) {
                val dB: Double = filterDB(i)
                var left = dB.toFloat()
                var right = dB.toFloat()
                val mid = height / 2f
                val x: Float = -offset + i * pitchSize + pitchSize / 2f
                var p = paint
                if (getDB(i) < 0) {
                    p = paintRed
                    left = 1f
                    right = 1f
                }
                if (editPos != -1 && i >= editPos) p = cutColor

                // left channel pitch
                canvas.drawLine(x, mid, x, mid - mid * left - 1, p)
                // right channel pitch
                canvas.drawLine(x, mid, x, mid + mid * right + 1, p)
            }

            // paint edit mark
            if (editPos != -1 && editFlash) {
                val x: Float = editPos * pitchSize + pitchSize / 2f
                canvas.drawLine(x, 0f, x, height.toFloat(), editPaint)
            }

            // paint play mark
            if (playPos > 0) {
                val x: Float = playPos * pitchSize + pitchSize / 2f
                canvas.drawLine(x, 0f, x, height.toFloat(), playPaint)
            }
        }
    }

    private open class HandlerUpdate : Runnable {
        var start: Long = 0
        var updateSpeed: Long = 0
        var mHandler: Handler? = null
        var run: Runnable? = null
        override fun run() {
            val cur = System.currentTimeMillis()
            val diff = cur - start
            start = cur
            var delay = updateSpeed + (updateSpeed - diff)
            if (delay > updateSpeed) delay = updateSpeed
            post(delay)
        }

        open fun post(delay: Long) {
            run?.run()
            if (delay > 0) mHandler?.postDelayed(this, delay) else mHandler?.post(this)
        }

        companion object {
            fun start(
                r: HandlerUpdate,
                handler: Handler,
                run: Runnable?,
                updateSpeed: Long
            ): HandlerUpdate {
                r.run = run
                r.start = System.currentTimeMillis()
                r.updateSpeed = updateSpeed
                r.mHandler = handler
                // post instead of draw.run() so 'start' will measure actual queue time
                handler.postDelayed(r, updateSpeed)
                return r
            }

            fun start(
                handler: Handler,
                run: Runnable?,
                updateSpeed: Long
            ): HandlerUpdate {
                val r = HandlerUpdate()
                return start(r, handler, run, updateSpeed)
            }

            fun stop(handler: Handler, run: Runnable?) {
                handler.removeCallbacks(run!!)
            }
        }
    }

    private class FallbackUpdate : HandlerUpdate() {
        var fallback: Runnable? = null
        var slow: Long = 0

        companion object {
            fun start(
                handler: Handler,
                run: Runnable?,
                fallback: Runnable?,
                updateSpeed: Long
            ): HandlerUpdate {
                val r = FallbackUpdate()
                r.fallback = fallback
                r.slow = updateSpeed / 4
                return start(
                    r,
                    handler,
                    run,
                    updateSpeed
                )
            }
        }

        override fun post(delay: Long) {
            if (delay < slow) {
                fallback!!.run()
            } else {
                this.run?.run()
            }
            if (delay > 0) this.mHandler?.postDelayed(this, delay) else this.mHandler?.post(this)
        }
    }

    companion object {
        val TAG = PitchView::class.java.simpleName

        // pitch delimiter length in dp
        const val PITCH_DELIMITER = 1f

        // pitch length in dp
        const val PITCH_WIDTH = 2f

        // update pitchview in milliseconds
        const val UPDATE_SPEED = 10

        // edit update time
        const val EDIT_UPDATE_SPEED = 250
    }

    var recColor = Color.GREEN
    var cutColor = Color.RED

    var mPitchTime = 0

    private var data: MutableList<Double> = LinkedList()

    // how many pitches we can fit on screen
    var pitchScreenCount = 0

    // how many pitches we should fit in memory
    var pitchMemCount = 0

    // pitch delimiter length in px
    var pitchDlimiter = 0

    // pitch length in px
    var pitchWidth = 0

    // pitch length in pn + pitch delimiter length in px
    var pitchSize = 0

    private var graph: PitchGraphView? = null
    private var current: PitchCurrentView? = null

    var time: Long = 0

    // how many samples were cut from begining of 'data' list
    var samples: Long = 0

    var edit: Runnable? = null

    // index
    var editPos = -1
    var editFlash = false

    // current playing position in samples
    var playPos = -1f
    private var play: Runnable? = null

    private var draw: Runnable? = null
    var offset = 0f

    private var mHandler = Handler()

    constructor(context: Context?) : this(context, null)

    constructor(
        context: Context?,
        attrs: AttributeSet?
    ) : this(context, attrs, 0)

    constructor(
        context: Context?,
        attrs: AttributeSet?,
        defStyleAttr: Int
    ) : super(context, attrs, defStyleAttr) {
        pitchDlimiter = DimensionConvert.dip2px(context!!, PITCH_DELIMITER)
        pitchWidth = DimensionConvert.dip2px(context, PITCH_WIDTH)
        pitchSize = pitchWidth + pitchDlimiter
        mPitchTime = pitchSize * UPDATE_SPEED
        graph = PitchGraphView(context)
        addView(graph)
        current = PitchCurrentView(context)
        current!!.setPadding(0, DimensionConvert.dip2px(context, 2f), 0, 0)
        addView(current)
        if (isInEditMode) {
            for (i in 0..2999) {
                data.add(-sin(i.toDouble()) * 90)
            }
            edit(150f)
        }
        time = System.currentTimeMillis()
    }

    fun getMaxPitchCount(width: Int): Int {
        val pitchScreenCount = width / pitchSize + 1
        return pitchScreenCount + 1
    }

    fun clear(s: Long) {
        data.clear()
        samples = s
        offset = 0f
        edit = null
        draw = null
        play = null
    }

    fun fit(max: Int) {
        if (max < 0) // -1
            return
        if (data.size > max) {
            val cut = data.size - max
            data.subList(0, cut).clear()
            samples += cut.toLong()
            val m = data.size - 1
            // screen rotate may cause play/edit offsets off screen
            if (editPos > m) editPos = m
            if (playPos > m) playPos = m.toFloat()
        }
    }

    fun add(a: Double) {
        data.add(a)
    }

    fun drawCalc() {
        graph?.calc()
        draw()
    }

    fun drawEnd() {
        fit(pitchMemCount)
        offset = 0f
        draw()
    }

    fun getEnd(): Int {
        var end = data.size - 1
        if (editPos != -1) {
            end = editPos
        }
        if (playPos > 0) {
            end = playPos.toInt()
        }
        return end
    }

    fun getDB(i: Int): Double {
        return data[i]
    }

    fun filterDB(i: Int): Double {
        var db = getDB(i)
        // do not show below NOISE_DB
        db -= 20
        if (db < 0) db = 0.0
        val rest: Int = 70
        db /= rest
        return db
    }

    fun draw() {
        graph!!.invalidate()
        current!!.invalidate()
    }

    fun getPitchTime(): Int {
        return mPitchTime
    }

    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec)
        val ww = measuredWidth - paddingRight - paddingLeft
        var hh = measuredHeight - paddingTop - paddingBottom
        current!!.measure(
            MeasureSpec.makeMeasureSpec(ww, MeasureSpec.AT_MOST),
            MeasureSpec.makeMeasureSpec(hh, MeasureSpec.AT_MOST)
        )
        hh -= current!!.measuredHeight
        graph!!.measure(
            MeasureSpec.makeMeasureSpec(ww, MeasureSpec.AT_MOST),
            MeasureSpec.makeMeasureSpec(hh, MeasureSpec.AT_MOST)
        )
    }

    override fun onLayout(
        changed: Boolean,
        l: Int,
        t: Int,
        r: Int,
        b: Int
    ) {
        graph!!.layout(
            paddingLeft, paddingTop,
            paddingLeft + graph!!.measuredWidth, paddingTop + graph!!.measuredHeight
        )
        current!!.layout(
            paddingLeft,
            graph!!.bottom,
            paddingLeft + current!!.measuredWidth,
            graph!!.bottom + current!!.measuredHeight
        )
    }

    fun stop() {
        if (edit != null) HandlerUpdate.stop(mHandler, edit)
        edit = null
        if (draw != null) HandlerUpdate.stop(mHandler, draw)
        draw = null
        if (play != null) HandlerUpdate.stop(mHandler, play)
        play = null
        draw()
    }

    fun edit(offset: Float): Long {
        editPos = if (offset < 0) -1 else offset.toInt() / pitchSize
        playPos = -1f
        if (editPos >= pitchScreenCount) editPos = pitchScreenCount - 1
        if (editPos >= data.size) editPos = data.size - 1
        if (draw != null) {
            HandlerUpdate.stop(mHandler, draw)
            draw = null
        }
        if (play != null) {
            HandlerUpdate.stop(mHandler, play)
            play = null
        }
        draw()
        edit()
        return samples + editPos
    }

    fun edit() {
        if (edit == null) {
            editFlash = true
            edit = FallbackUpdate.start(mHandler, Runnable {
                draw()
                editFlash = !editFlash
            }, Runnable {
                graph!!.calc()
                editFlash = !editFlash
            }, EDIT_UPDATE_SPEED.toLong())
        }
    }

    fun record() {
        if (edit != null) HandlerUpdate.stop(mHandler, edit)
        edit = null
        editPos = -1
        if (play != null) HandlerUpdate.stop(mHandler, play)
        play = null
        playPos = -1f
        if (draw == null) {
            time = System.currentTimeMillis()
            draw = FallbackUpdate.start(
                mHandler, Runnable { drawCalc() },
                Runnable { graph!!.calc() }, UPDATE_SPEED.toLong()
            )
        }
    }

    // current paying pos in actual samples
    fun play(pos: Float) {
        if (pos < 0) {
            playPos = -1f
            if (play != null) {
                HandlerUpdate.stop(mHandler, play)
                play = null
            }
            if (edit == null) {
                edit()
            }
            return
        }
        playPos = pos - samples
        editFlash = true
        val max = data.size - 1
        if (playPos > max) playPos = max.toFloat()
        if (edit != null) HandlerUpdate.stop(mHandler, edit)
        edit = null
        if (draw != null) HandlerUpdate.stop(mHandler, draw)
        draw = null
        if (play == null) {
            time = System.currentTimeMillis()
            play = FallbackUpdate.start(
                mHandler, Runnable { draw() },
                Runnable { graph!!.calc() }, UPDATE_SPEED.toLong()
            )
        }
    }

}